/**
* JimBot - Java IM Bot
* Copyright (C) 2006-2009 JimBot project
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/
package ru.jimbot.protocol;

import com.google.gson.JsonArray;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.CookieStore;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.params.ClientPNames;
import org.apache.http.client.params.CookiePolicy;
import org.apache.http.client.protocol.ClientContext;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.BasicCookieStore;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.protocol.BasicHttpContext;
import org.apache.http.protocol.HttpContext;
import ru.jimbot.modules.AbstractProps;
import ru.jimbot.modules.MsgOutQueue;
import ru.jimbot.util.Log;
import ru.jimbot.util.MainProps;

/**
 * Реализация VK-протокола
 * @author alex_xpert
 */
public class VKProtocol extends AbstractProtocol {
    private static AbstractProps props;
    
    private static final String USER_AGENT_PC = "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:26.0) Gecko/20100101 Firefox/26.0";
    private static final String USER_AGENT_MOBILE = "Mozilla/5.0 (Linux; U; Android 4.1.1; ru-ru; ALCATEL ONE TOUCH 5020D Build/JRO03C) AppleWebKit/534.30 (KHTML, like Gecko) Version/4.1 Mobile Safari/534.30";
    
    private static HttpClient client;
    private static HttpResponse res;
    private static HttpGet get;
    private static HttpPost post;
    private static JsonParser parser;
    private static VKMessageListener vkml;
    private static Thread t = null;
    
    private static String LOGIN; // Телефон или E-mail
    private static String PASSWORD; // Пароль
    
    private static final String APP_ID = "4074314"; // Тут ID вашего приложения
    private static final String PERMISSIONS = "messages,status,wall,offline"; // http://vk.com/dev/permissions
    private static final String REDIRECT_URI = "https://oauth.vk.com/blank.html"; // Заглушка для Standalone-приложений
    private static final String DISPLAY = "mobile"; // page|popup|mobile
    private static final String API_VERSION = "5.8"; // Последняя на данный момент
    private static final String RESPONSE_TYPE = "token"; // Есть ещё code, но это для сайтов
    
    private static String link = "http://oauth.vk.com/authorize?"
                               + "client_id=" + APP_ID
                               + "&scope=" + PERMISSIONS
                               + "&redirect_uri=" + REDIRECT_URI
                               + "&display=" + DISPLAY
                               + "&v=" + API_VERSION
                               + "response_type=" + RESPONSE_TYPE;
    
    private static String ACCESS_TOKEN = null;
    
    public VKProtocol(AbstractProps props){
        this.props = props;
        mq = new MsgOutQueue(this, props.getIntProperty("bot.pauseOut"),
        props.getIntProperty("bot.pauseRestart"),
        props.getIntProperty("bot.msgOutLimit"));
        client = getHttpClient();
        parser = new JsonParser();
    }

    /**
     * Возвращает параметры сервиса
     */
    @Override
    public AbstractProps getProps(){
        return props;
    }

    /**
     * Возвращает размер очереди исходящих сообщений
     */
    @Override
    public int getOuteqSize(){
        return mq.size();
    }

    /**
     * Подключение
     */
    @Override
    public void connect() {
        try {
            LOGIN = screenName;
            PASSWORD = password;
            mq.start();
            if(getAccessToken() != null) {
                vkml = new VKMessageListener(this);
                t = new Thread(vkml);
                t.setDaemon(true);
                t.setName("vk");
                onLogin();
            }
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Переподключение
     */
    @Override
    public void reConnect(){
        if(this.isOnLine()) {
            disconnect();
        }
        try {
            Thread.sleep(5000);
        } catch(Exception ex) {
            ex.printStackTrace();
        }
        this.connect();
    }

    /**
     * Отключение
     */
    @Override
    public void disconnect() {
        if(t == null) return;
        try {
            mq.stop();
            vkml.run = false;
            t = null;
        }catch (Exception ex){
            ex.printStackTrace();
        }
    }

    /**
     * Прием сообщения
     */
    @Override
    public void getMsg(String sendSN, String recivSN, String msg, boolean isOffline){
        protList.getMsg(sendSN, recivSN, msg, isOffline);
    }

    /**
     * Подключен?
     */
    @Override
    public boolean isOnLine() {
        if(t == null) return false;
        return connected;
    }

    /**
     * Отправка сообщения
     */
    @Override
    public void sendMsg(String sn, String msg){
        try{
            post = new HttpPost("https://api.vk.com/method/messages.send");
            List<NameValuePair> formdata = new ArrayList<NameValuePair>();
            formdata.add(new BasicNameValuePair("user_id", sn));
            formdata.add(new BasicNameValuePair("chat_id", "1"));
            formdata.add(new BasicNameValuePair("message", msg));
            formdata.add(new BasicNameValuePair("type", "1"));
            //formdata.add(new BasicNameValuePair("guid", "1"));
            formdata.add(new BasicNameValuePair("access_token", getAccessToken()));
            UrlEncodedFormEntity form = new UrlEncodedFormEntity(formdata, "UTF-8");
            post.setEntity(form);
            client.execute(post);
            post.releaseConnection();
        }catch (Exception e){
            Log.info("ERROR send message: " + msg);
        }
    }

    /**
     * Запрашивать авторизацию?
     */
    @Override
    public boolean isNoAuthUin(String uin){
        return props.getBooleanProperty("chat.isAuthRequest");
    }

    /*
     * Обработка входящего сообщения
     */ 
    public void onIncomingMessage(String user_id, String msg) {
        try {
            if(MainProps.isIgnor(user_id)) {
                Log.flood2("IGNORE LIST: " + screenName + ": " + msg);
            return;
            }
            protList.getMsg(user_id, screenName, msg, false);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }


    /**
     * Ошибка подключения
     */
    public void onAuthorizationFailed(String reason){
        System.out.println("Ошибка авторизации: " + reason);
        connected = false;
    }

    /**
     * Смена статуса
     */
    public void onStatusChange(){
    }

    /**
     * Обработка входа
     */
    @Override
    public void onLogin() {
        connected = true;
        Log.info("VK Account " + baseUin + " Online");
        t.start();
        setStatus(0);
    }

    /**
     * Обработка выхода
     */
    public void onLogout(Exception e) {
        Log.error("Разрыв соединения: " + screenName + " - " + server + ":" + port
        + " По причине: " + e.getMessage().toString());
        try {
            Log.info("Переподключение через 1 минуту...");
            Thread.sleep(60000);
            this.reConnect();
        } catch (Exception ex) {
            connected = false;
        }
    }

    /**
     * Установка X-статуса
     * @param n - номер х-статуса
     * @param text - текст х-статуса
     */
    public void setXStatus(int n, String text){}

   /**
    * Оффлайн сообщение
    * @param e 
    */
    public void onOfflineMessage() {}
    
    public void getStatus(String sn, int status) {}

    @Override
    public void setStatus(int status) {
        String text = props.getStringProperty("icq.STATUS_MESSAGE2");
        try {
            post = new HttpPost("https://api.vk.com/method/status.set");
            List<NameValuePair> formdata = new ArrayList<NameValuePair>();
            formdata.add(new BasicNameValuePair("text", text));
            formdata.add(new BasicNameValuePair("access_token", getAccessToken()));
            UrlEncodedFormEntity form = new UrlEncodedFormEntity(formdata, "UTF-8");
            post.setEntity(form);
            client.execute(post);
            post.releaseConnection();
        } catch(Exception ex) {
            ex.printStackTrace();
        }
    }

    @Override
    public void addContactList(String sn) {}

    @Override
    public void RemoveContactList(String sn) {}

    @Override
    public boolean userInfoRequest(String paramString1, String paramString2) {
        return false;
    }

    @Override
    public void authRequest(String paramString1, String paramString2) {}
    
    /**
     * Возвращает access_token
     */
    private static String getAccessToken() {
        if(ACCESS_TOKEN == null) {
            try {
                HttpClient client = getHttpClient();
                HttpContext context = getHttpContext();
                
                // step 1
                HttpGet get = new HttpGet(link);
                HttpResponse response = client.execute(get, context);
                InputStream is = response.getEntity().getContent();
                StringBuffer content = new StringBuffer();
                int i;
                while((i = is.read()) != -1) content.append((char) i);
                String ip_h = content.toString().split("name=\"ip_h\" value=\"")[1].split("\"")[0];
                String to = content.toString().split("name=\"to\" value=\"")[1].split("\"")[0];
                link = "http://login.vk.com/?act=login&soft=1&utf8=1";
                content = new StringBuffer();
                get.releaseConnection();

                //step 2
                HttpPost post = new HttpPost(link);
                List<NameValuePair> formdata = new ArrayList<NameValuePair>();
                formdata.add(new BasicNameValuePair("_origin", "http://oauth.vk.com"));
                formdata.add(new BasicNameValuePair("ip_h", ip_h));
                formdata.add(new BasicNameValuePair("to", to));
                formdata.add(new BasicNameValuePair("email", LOGIN));
                formdata.add(new BasicNameValuePair("pass", PASSWORD));
                UrlEncodedFormEntity form = new UrlEncodedFormEntity(formdata);
                post.setEntity(form);
                response = client.execute(post);
                link = response.getFirstHeader("Location").getValue();
                post.releaseConnection();
                content = new StringBuffer();
                
                // Шаг 3
                get = new HttpGet(link);
                response = client.execute(get, context);
                is = response.getEntity().getContent();
                while((i = is.read()) != -1) content.append((char) i);
                if (response.getFirstHeader("Location") != null) {
                    link = response.getFirstHeader("Location").getValue().replace("code", "token"); // Глюк ВКонтакте
                } else {
                    link = content.toString().split("<form method=\"post\" action=\"")[1].split("\"")[0].replace("code", "token"); // Глюк Вконтакте
                }
                content = new StringBuffer();
                get.releaseConnection();
                
                // Шаг 4
                get = new HttpGet(link);
                response = client.execute(get);
                get.releaseConnection();
                ACCESS_TOKEN = response.getFirstHeader("Location").getValue().split("access_token=")[1].split("&")[0];
            } catch(Exception ex) {
                ex.printStackTrace();
            }
        }
        return ACCESS_TOKEN;
    }
    
    /**
     * Возвращает сконфигурированный HttpClient
     */
    private static HttpClient getHttpClient() {
        HttpClient client = null;
        try {
            HttpClient httpclient = new DefaultHttpClient();
            ClientConnectionManager baseCcm = httpclient.getConnectionManager();
            SchemeRegistry sr = baseCcm.getSchemeRegistry();
            ClientConnectionManager safeCcm = new ThreadSafeClientConnManager(sr);
            /**
             * Принимаем все сертификаты
             */
            X509TrustManager dontCareTrustManager = new X509TrustManager() {
                private final X509Certificate[] empty = new X509Certificate[0];

                @Override
                public X509Certificate[] getAcceptedIssuers() { return empty; }

                @Override
                public void checkServerTrusted(X509Certificate[] ar, String st) throws CertificateException {}

                @Override
                public void checkClientTrusted(X509Certificate[] ar, String st) throws CertificateException {}
            };
            SSLContext ctx = SSLContext.getInstance("TLS");
            ctx.init(null, new TrustManager[] { dontCareTrustManager }, null);
            SSLSocketFactory ssf = new SSLSocketFactory(ctx);
            sr.register(new Scheme("https", 443, ssf));
            httpclient = new DefaultHttpClient(safeCcm, httpclient.getParams());
            httpclient.getParams().setParameter(ClientPNames.COOKIE_POLICY, CookiePolicy.BROWSER_COMPATIBILITY);
            httpclient.getParams().setParameter(ClientPNames.HANDLE_REDIRECTS, false);
            httpclient.getParams().setParameter(CoreProtocolPNames.USER_AGENT, props.getStringProperty("vk.online").equals("pc") ? USER_AGENT_PC : USER_AGENT_MOBILE);
            client = httpclient;
        } catch(Exception ex) {
            ex.printStackTrace();
        }
        return client;
    }
    
    /**
     * Возвращает сконфигурированный HttpContext
     */
    private static HttpContext getHttpContext() {
        HttpContext localContext = new BasicHttpContext();
        CookieStore cookieStore = new BasicCookieStore();
        localContext.setAttribute(ClientContext.COOKIE_STORE, cookieStore);
        return localContext;
    }
    
    class VKMessageListener implements Runnable {
        private String key;
        private String server;
        private String ts;
        private VKProtocol vkprot;
        
        public boolean run = true;
        private JsonObject result;
        
        private Timer timer = new Timer();
        
        public VKMessageListener(VKProtocol vkprot) {
            this.vkprot = vkprot;
            getLongPollServer();
            if(props.getStringProperty("vk.online").equals("off")) {
                timer.schedule(new VKSetOfflineStatus(), 10);
            } else {
                timer.schedule(new VKUpdateOnlineStatus(), 10, 900000);
            }
        }

        private void getLongPollServer() {
            try {
                // Запрашиваем данные для подключения к long-poll серверу
                get = new HttpGet("https://api.vk.com/method/messages.getLongPollServer?use_ssl=1?need_pts=0&access_token=" + getAccessToken());
                res = client.execute(get);
                get.releaseConnection();
                result = parser.parse(new InputStreamReader(res.getEntity().getContent())).getAsJsonObject().getAsJsonObject("response");
                this.key = result.get("key").getAsString();
                this.server = result.get("server").getAsString();
                this.ts = result.get("ts").getAsString();
            } catch(Exception ex) {
                ex.printStackTrace();
            }
        }
        
        @Override
        public void run() {
            while(run) {
                try {
                    // Пинаем long-poll сервер
                    get = new HttpGet("https://" + server + "?act=a_check&key=" + key + "&ts=" + ts + "&wait=25&mode=0");
                    res = client.execute(get);
                    result = parser.parse(new InputStreamReader(res.getEntity().getContent())).getAsJsonObject();
                    if(result.has("failed")) { // Истекло время действия ключа для подключения к LongPoll серверу
                        getLongPollServer();
                        continue;
                    }
                    ts = result.get("ts").getAsString();
                    JsonArray updates = result.getAsJsonArray("updates");
                    for(int i=0;i<updates.size();i++) {
                        JsonArray update = updates.get(i).getAsJsonArray();
                        if(update.get(0).getAsInt() == 4 && update.get(2).getAsInt() % 4 == 1) { // Входящее сообщение
                            vkprot.onIncomingMessage(update.get(3).getAsString(), update.get(6).getAsString());
                            client.execute( // Помечаем как прочитанное
                                    new HttpGet("https://api.vk.com/method/messages.markAsRead"
                                            + "?message_ids=" + update.get(1).getAsString()
                                            + "&user_id=" + update.get(3).getAsString()
                                            + "&access_token=" + getAccessToken()));
                        }
                    }
                    t.sleep(1000);
                } catch(Exception ex) {
                    ex.printStackTrace();
                }
            }
        }
        
        class VKUpdateOnlineStatus extends TimerTask {

            @Override
            public void run() {
                try {
                    if(!run) cancel();
                    get = new HttpGet("https://api.vk.com/method/account.setOnline?voip=0&access_token=" + getAccessToken());
                    client.execute(get);
                    get.releaseConnection();
                } catch(Exception ex) {
                    ex.printStackTrace();
                }
            }

        }
        
        class VKSetOfflineStatus extends TimerTask {

            @Override
            public void run() {
                try {
                    get = new HttpGet("https://api.vk.com/method/account.setOffline?access_token=" + getAccessToken());
                    client.execute(get);
                    get.releaseConnection();
                } catch(Exception ex) {
                    ex.printStackTrace();
                }
            }

        }
        
    }

}
